package hxm.core;

import haxe.macro.Context;
import haxe.macro.Expr;
import hxm.core.MacroHelper;

import tink.core.types.Outcome;

import hxm.core.MExpr;
import hxm.monads.OptionMonad;

/**
 * ...
 * @author Simon Krajewski
 */

class Resolver 
{
	#if macro
	
	static var aliases:Hash<String> = new Hash();
	static var cache:Hash<hxm.core.MExpr.MonadicClass> = new Hash();
	static var paths:Array<String> = [];
	static var funcs:Hash<Expr> = new Hash();
	
	static public function addPath(path:String)
	{
		paths.push(path);
	}
	
	static public function defineAlias(alias:String, to:String)
	{
		if (alias == to)
			Context.error("Self alias: " +alias, Context.currentPos());
		aliases.set(alias, to);
	}
	
	static public function defineFunc(name:String, func:Expr)
	{
		funcs.set(name, func);
	}
	
	static public function getFunc(name:String) return funcs.exists(name) ? Some(funcs.get(name)) : None
	
	static public function resolveMonadicClass(name:String):Option<MonadicClass>
	{
		name = follow(name);
		
		if (cache.exists(name)) return Some(cache.get(name));
		
		for (i in 0...paths.length)
		{
			var path = paths[paths.length - i - 1];
			var clsName = path + "." +name + "Monad";
			try {
				var cls = Context.getType(clsName);
				cache.set(name, {path:clsName, type:cls});
				return Some(cache.get(name));
			} catch (e:Dynamic)
			{

			}
		}
		return None;
	}
	
	static public function unify(n:String, o:String):Outcome<String, String>
	{
		n = follow(n);
		o = follow(o);

		var nType:MonadicClass = switch(resolveMonadicClass(n))
		{
			case None:
				return Failure("No type found for " +n);
			case Some(type):
				type;
		}
		var oType = switch(resolveMonadicClass(o))
		{
			case None:
				return Failure("No type found for " +o);
			case Some(type):
				type;
		}		
		
		if (n == o) return Success(n);

		var nUni = MacroHelper.getMetaStringValues(nType.type, "unify");
		nUni.unshift(n);
		var oUni = MacroHelper.getMetaStringValues(oType.type, "unify");
		oUni.unshift(o);	

		for (nMeta in nUni)
		{
			for (oMeta in oUni)
			{
				if (oMeta == o && nMeta == n) continue;
				switch(unify(oMeta, nMeta))
				{
					case Success(uni):
						return Success(uni);
					case Failure(err):
				}
			}
		}

		return Failure("Could not unify.");
	}
	
	static function follow(t:String) return aliases.exists(t) ? follow(aliases.get(t)) : t
	
	#end
}